Genesis 3D |
||
per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto! |
Undicesima
lezione
Per commentare, dare suggerimenti
o consigli (molto apprezzati) e per segnalare eventuali errori o
disattenzioni (sempre possibili), puoi inviare una email all'autore (clicca
sul nome sopra). Grazie per la collaborazione!
In questo tutorial vedremo
come:
1) rilevare pressione dei tasti e movimento
del mouse
2) muovere la telecamera attorno
all'ambiente
3) rilevare le
collisioni
4) applicare la gravità
Le spiegazioni saranno fornite in modo
incrementale rispetto al primo tutorial. Forniremo quindi sole li informazioni
aggiuntive e non l'intero codice.
Il primo posto nella quale la pressione dei
tasti viene rilevata è la funzione WndProc, richiamata da windows stessa quando
ci sono dei messaggi in attesa. Tale funzione modifica lo stato dell'elemento di
un array corrispondente al tasto premuto. TRUE = premuto, FALSE =
rilasciato.
Tale array si trova nella struttura
InputOutput e viene istanziato nella variabile io. Quindi per accedere, ad
esempio, alla freccia in su della tastiera si deve leggere il valore di
io.keys[VK_UP]. La costante VK_UP viene definita in Winuser.h; ne diamo un
piccolo esempio:
#define
VK_SPACE
0x20
#define
VK_PRIOR
0x21
#define
VK_NEXT
0x22
#define
VK_END 0x23
#define
VK_HOME
0x24
#define
VK_LEFT
0x25
#define
VK_UP
0x26
#define
VK_RIGHT
0x27
#define
VK_DOWN
0x28
#define
VK_SELECT
0x29
#define
VK_PRINT
0x2A
#define
VK_EXECUTE
0x2B
#define
VK_SNAPSHOT
0x2C
#define
VK_INSERT
0x2D
#define
VK_DELETE
0x2E
#define
VK_HELP
0x2F
Mentre la pressione dei tasti ci serve per
muovere la telecamera avanti e indietro rispetto ad una posizione iniziale e un
vettore di orientamento, il mouse può essere utile per direzionare tale vettore.
Movendo a destra e a sinistra il mouse il vettore di orientamento viene ruotato
in senso orario e antiorario.
Definiamo nella main.h la costante
sensibilità del mouse:
#define MOUSESENSITIVITY 0.002f;
Questo valore potremmo in una versione
successiva leggerlo direttamente da file.
Per leggere la posizione del mouse
utilizzeremo la funzione GetCursorPos(POINT *posizione) che fa parte delle API
di windows. Dopo la chiamata posizione contiene le coordinate schermo del
puntatore del mouse.
Per rilevare i movimenti relativi del mouse
prima poliamo il cursore al centro dello schermo, poi leggiamo la posizione e
calcoliamo la differenza in termini di deltaX e deltaY. A questo punto
moltiplichiamo deltaX e deltaY per la sensitività del mouse e otteniamo lo
spostamento relativo nelle due dimensioni.
Adesso vedremo come utilizzare questi
dati.
Associamo alla telecamera due vettori. Il
primo vettore indica la posizione della telecamera nello spazio globale. In
realtà la telecamera la solleviamo rispetto a tale posizione di un certo valore
che rappresenta la nostra altezza virtuale. Quindi il vettore posizione indica
la posizione dei piedi e non della telecamera.
Il secondo vettore è l'angolo, ovvero
l'orientamento della telecamera. Va immaginato applicato al punto in cui giace
la telecamera e indica la direzione dello sguardo. Utilizzare un vettore,
anziché un semplice valore float ci permette di avere 2 gradi di libertà
(destra-sinistra e alto basso).
Avevamo già definito la struttura Player
come
typedef struct
{
geVec3d posizione; //
Posizione del giocatore
geVec3d angolo;
// direzione sguardo
geVec3d Mins;
// Bounding box
geVec3d Maxs;
int height;
//how tall are we
int speed;
//how fast are we
int caduta;
// velocità di caduta
int salto;
// altezza del salto
int gradino;
// altezza massima del gradino che può salire
int stato;
int counter;
}
Player;
I due vettori di cui parlo sono posizione e
angolo.
Iniziamo con ordine, analizzando la funzione
WinMain(). In marrone segno le differenze e le aggiunte rispetto alla prima
versione.
int
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,
int nShowCmd)
{
LoadPrefs(io.driver, &io.Width, &io.Height,
io.map);
CreateMainWindow(hInstance,nShowCmd);
//Initialize the
Genesis engine
InitEngine(hWnd);
Setup();
MainLoop();
//Shutdown the
Genesis engine and clean up the memory
Shutdown();
//End
Program
return
1;
}
L'unica differenza sta nella funzione Setup()
nella quale diamo un valore ai campi di player.
void
Setup(void) {
// posizione
e orientamento inziale del personaggio
pl.posizione.X =
0.0f;
pl.posizione.Y =
100.0f;
pl.posizione.Z =
0.0f;
pl.angolo.X =
0.0f;
pl.angolo.Y =
0.0f;
pl.angolo.Z =
0.0f;
// impostazione
del bounding box attorno al personaggio
pl.Mins.X =
-20.0f;
pl.Mins.Y
= 0.0f;
pl.Mins.Z =
-20.0f;
pl.Maxs.X =
20.0f;
pl.Maxs.Y =
175.0f;
pl.Maxs.Z =
20.0f;
// We will set
this values up here only for an easier transition later.
pl.height = 170; // this is actually our
players eye level, and not real height
pl.speed = 4;
pl.caduta =
6;
pl.gradino =
41;
pl.salto =
70;
// stato
inziale
pl.stato =
standing;
}
La posizione del player è fissata in
(0,100,0) in modo che l'effetto iniziale sia quello dell'uomo che cade verso il
basso. Questi valori naturalmente possono essere diversi. Il loro valore dipende
dalla mappa che viene usata nell'applicazione. Scegliere un punto di inizio
adeguato.
L'angolo setta la direzione dello sguardo
all'inizio.
Mins e Maxs verranno spiegato dopo. Le altre
variabili rappresentando dei parametri come la velocità, l'altezza,
ecc...
Lo stato del player viene fissato a
"standing".
In questa funzione i cambiamenti sono già più
sostanziali.
void
MainLoop() {
MSG
msg;
int
run;
geBoolean coll;
//Main game
loop
run =
1;
while
(run)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { // Is There A Message
Waiting?
if (msg.message==WM_QUIT) {
// Have We Received A Quit Message?
run = 0;
// If So done=TRUE
} else {
// If Not, Deal With Window Messages
TranslateMessage(&msg);
// Translate The Message
DispatchMessage(&msg);
// Dispatch The Message
}
} else {
// If There Are No Messages
if (pl.stato == standing || pl.stato == falling) {
coll = Gravity((float)pl.caduta);
if (coll == GE_TRUE)
pl.stato =
standing;
}
// Aggiorna l'ambiente
MoveCamera();
AggiornaXForm(); // sposta la
camera
Prima di fare il rendering
controlliamo lo stato del player. Se esso è "standing" oppure "falling" allora
applichiamo la gravità.
La funzione gravity fa
esattamente quello che ci si aspetta: verifica se il player si trova su qualche
cosa, altrimenti lo fa "cadere" di una certa quantità (in relazione alle leggi
di gravitazione e al buon senso). Se il player atterra su qualche cosa allora
restituisce TRUE.
MoveCamera aggiorna la
posizione del player in base all'input da tastiera e
mouse.
AggiornaXForm applica le
trasformazioni alla telecamera.
if
(!geEngine_BeginFrame(Engine, Camera, GE_FALSE))
run = 0;
if (!geEngine_RenderWorld(Engine, World, Camera,
0.0f))
run = 0;
if (!geEngine_EndFrame(Engine))
run = 0;
if (io.keys[VK_ESCAPE]) {
run=0;
}
}
}
}
Ecco la nuova funzione MoveCamera, usata per
aggiornare la posizione del player in base alla tastiera e al
mouse.
void
MoveCamera(void) {
float
forwardmove;
// movimento in avanti
geVec3d
angolo;
// angolo testa
geVec3d
normale;
geVec3d
newpos;
geBoolean
coll = GE_FALSE;
angolo =
MoveHead();
// controlla il mouse
pl.angolo.Y +=
angolo.Y;
pl.angolo.X +=
angolo.X;
//make
sure we arent looking too far up or down. If we are then fix
that!
if (pl.angolo.X
>0.9f) pl.angolo.X
=0.9f;
if (pl.angolo.X
<-0.9f) pl.angolo.X =-0.9f;
La prima operazione da fare è
verificare lo spostamento del mouse attraverso la funzione MoveHead(). Essa
restituisce un vettore che rappresenta lo spostamento angolare relativo nelle
direzioni X (sopra-sotto) e Y (destra-sinistra).
Naturalmente imponiamo dei
limiti all'angolo X in modo che lo sguardo non possa andare oltre un certo
range.
if
(pl.stato == standing) {
//pl.posizione.Y
+= pl.gradino;
forwardmove = CheckKeyboard();
// controlla la tastiera
normale = Player_Normal(FRONT);
geVec3d_AddScaled(&pl.posizione,
&normale, forwardmove, &newpos);
pl.posizione=ControllaCollisione(pl.posizione,newpos,&coll);
// verifica le collisioni
Secondo passo è quello di
verificare la pressione dei tasti freccia in alto, freccia in basso mediante la
funzione CheckKeyboard() che restituisce la "quantità di spostamento" da
effettuare. Un valore negativo indica un movimento verso
dietro.
Quindi viene rilevato il
vettore frontale al player (che naturalmente dipende dall'angolo) e il vettore
posizione viene aggiornato sommando ad esso il vettore dello
spostamento.
La funzione AddScaled effettua
questa operazione newpos = pl.posizione + (normale *
forwardmove).
Poi viene verificata la
collisione con eventuali oggetti (muri, porte...), ma ce ne occuperemo
dopo.
if (coll) {
pl.posizione.Y += pl.gradino;
forwardmove = CheckKeyboard();
// controlla la tastiera
normale = Player_Normal(FRONT);
geVec3d_AddScaled(&pl.posizione, &normale, forwardmove,
&newpos);
pl.posizione=ControllaCollisione(pl.posizione,newpos,&coll);
// verifica le collisioni
Gravity((float)pl.gradino);
// usata così questa funzione serve a muovere il giocatore verso il
basso
// di un certo valore fino a fargli toccare il
terreno
}
}
}
Anche questa parte è dedicata
alle collisioni; il suo scopo è quello di salire i gradini. Il problema
essenziale dei gradini è che quando ci ha una collisione con un gradino, ci deve
essere un metodo per verificare che si tratta di un ostacolo superabile, mentre
un muro non lo è. La soluzione riportata (è chiaro che ce ne possono essere
delle altre) è la seguente. Qualora ci sia una collisione, si tenta di andare
ugualmente nella direzione, ma salendo la posizione di un valore pl.gradino, che rappresenta il massimo
dislivello che si può superare. Se la collisione persiste, allora si tratta di
un muro, altrimenti vuol dire che si tratta di un dislivello superabile e si
applica la funzione gravità per arrivare a "toccare" la superficie superiore del
gradino stesso.
Il comportamento di tale
funzione è stata già accennata quando si è parlato di mouse.
geVec3d
MoveHead(void)
//Moves the camera base on our mouse position
{
POINT
temppos;
geVec3d
TempAngles;
geFloat
TURN_SPEED;
// speed camera will move if turning left/right
geFloat
UPDOWN_SPEED;
// speed camera will move if looking up/down
int
screen_x,screen_y;
int
tempscreen_x,tempscreen_y;
TempAngles.X =
0.0f;
TempAngles.Y =
0.0f;
TempAngles.Z =
0.0f;
GetCursorPos(&temppos); // get
the mouse position in SCREEN coordinates
tempscreen_x=temppos.x;
tempscreen_y=temppos.y;
screen_x =
io.Width/2;
// calculate the center of the screen
screen_y =
io.Height/2;
// calculate the center of the screen
SetCursorPos(screen_x, screen_y);
// set the cursor in the center of the screen
TURN_SPEED = abs(tempscreen_x-screen_x) *
MOUSESENSITIVITY; //
calculate the turning speed
UPDOWN_SPEED =
abs(tempscreen_y-screen_y) * MOUSESENSITIVITY; // calculate the up/down
speed
if ((tempscreen_x
!= screen_x) || (tempscreen_y != screen_y))
{
if (tempscreen_x > screen_x) // is it to the
left?
TempAngles.Y = -(geFloat)(TURN_SPEED); //if so spin
left
else if (tempscreen_x < screen_x) // is it to the
right?
TempAngles.Y = (geFloat)(TURN_SPEED); //if so spin
right
if (tempscreen_y > screen_y) // is it to the
top?
TempAngles.X = -(geFloat)(UPDOWN_SPEED); //if so look up
else if (tempscreen_y < screen_y) // is it to the
bottom?
TempAngles.X = (geFloat)(UPDOWN_SPEED); //if so look down
}
return
TempAngles;
}
Funzione piuttosto semplice: verifica lo
stato dei tasti e determina lo spostamento in base alla velocità del
player.
float
CheckKeyboard() {
int speed =
pl.speed;
float
forwardspeed=0;
if
(io.keys[VK_LBUTTON] || io.keys[VK_CONTROL])
speed = pl.speed*2;
if
(io.keys[VK_UP])
forwardspeed += speed;
if
(io.keys[VK_DOWN])
forwardspeed -= pl.speed;
if
(io.keys[VK_SPACE])
if (pl.stato == standing)
player_jump_start();
return
forwardspeed;
}
Qualora venga premuto il tasto CTRL la
velocità viene raddoppiata simulando l'effetto corsa. La funzione restituisce lo
spostamento.
Tale funzione riceve in ingresso la direzione
di cui si vuole il versore. Le possibilità sono: UP,
FRONT, LEFT. Prendiamo ad esempio la direzione FRONT; la funzione restituisce la
proiezione dell'angolo rispetto al piano X-Z.
geVec3d
Player_Normal(int direction) {
// usa la
variabile globale pl per determinare la direzione "basso"
geXForm3d
XForm;
geVec3d
normale;
geXForm3d_SetIdentity(&XForm);
geXForm3d_RotateY(&XForm,pl.angolo.Y);
switch
(direction) {
case
UP:
geXForm3d_GetUp(&XForm,&normale);
break;
case
FRONT:
geXForm3d_GetIn(&XForm,&normale);
break;
case
LEFT:
geXForm3d_GetLeft(&XForm,&normale);
break;
}
return
normale;
}
Questo viene sostanzialmente
effettuato mediante la funzione geXForm3d_Getxx dove al posto di xx si mette Up,
In o Left.
Costruisce la matrice di
trasformazione in base a io.posizione e io.angolo. Tiene conto dell'altezza del
player e posiziona la camera nello spazio globale mediante la matrice così
costruita.
void
AggiornaXForm() {
geXForm3d
ViewXForm;
geXForm3d_SetIdentity(&ViewXForm);
//Setup the
rotation
geXForm3d_RotateX(&ViewXForm, pl.angolo.X);
geXForm3d_RotateY(&ViewXForm, pl.angolo.Y);
geXForm3d_RotateZ(&ViewXForm, pl.angolo.Z);
geXForm3d_Translate(&ViewXForm, pl.posizione.X-100,
pl.posizione.Y+pl.height, pl.posizione.Z);
//Set the XForm
to the camera
geCamera_SetWorldSpaceXForm(Camera,
&ViewXForm);
}
Durante il movimento della
telecamera nell'ambiente risulta indispensabile effettuare il controllo delle
collisioni con gli elementi presenti nell'ambiente stesso: muri, scalini
ecc...
Il controllo delle collisioni è demandato all'engine mediante una sola funzione: geWorld_Collision, la cui sintassi d'uso è (dalla documentazione originale)
GENESISAPI geBoolean
geWorld_Collision(geWorld *World, const geVec3d
*Mins, const geVec3d *Maxs, const geVec3d *Front, const geVec3d *Back, uint32
Contents, uint32 CollideFlags, uint32 UserFlags, GE_CollisionCB *CollisionCB,
void *Context, GE_Collision *Collision);
dove:
World è il puntatore
all'oggetto geWorld con il quale si vuole determinare la
collisione.
Mins e Maxs rappresentano i
vertici che definiscono un parallelepipedo. La collisione verrà determinata tra
l'area del parallelepipedo e gli oggetti presenti nell'ambiente. Le coordinate
del parallelepipedo hanno come riferimento uno spazio di stato locale. Esse
devono riferirsi come spazio di stato globale ad altri due parametri: front e
back.
Front e Back rappresentano due
parametri con una duplice funzione. Possono essere usati per una collisione
"lineare" anziché "volumetrica" oppure possono essere usati per fornire un
riferimento di coordinate ai parametri Mins e Maxs.
Contents è una combinazione di
alcune costanti definite in genesis.h
#define
GE_CONTENTS_SOLID (1<<0)
// Solid (Visible)
#define
GE_CONTENTS_WINDOW (1<<1)
// Window (Visible)
#define
GE_CONTENTS_EMPTY (1<<2)
// Empty but Visible (water, lava, etc...)
#define
GE_CONTENTS_TRANSLUCENT (1<<3) // Vis will see through it
#define
GE_CONTENTS_WAVY (1<<4)
// Wavy (Visible)
#define
GE_CONTENTS_DETAIL (1<<5)
// Won't be included in vis oclusion
#define
GE_CONTENTS_CLIP (1<<6)
// Structural but not visible
#define
GE_CONTENTS_HINT (1<<7)
// Primary splitter (Non-Visible)
#define
GE_CONTENTS_AREA (1<<8)
// Area seperator leaf (Non-Visible)
//
These contents are all solid types
#define
GE_CONTENTS_SOLID_CLIP (GE_CONTENTS_SOLID | GE_CONTENTS_WINDOW |
GE_CONTENTS_CLIP)
#define
GE_CONTENTS_CANNOT_OCCUPY GE_CONTENTS_SOLID_CLIP
//
These contents are all visible types
#define
GE_VISIBLE_CONTENTS (GE_CONTENTS_SOLID | GE_CONTENTS_EMPTY | GE_CONTENTS_WINDOW
| GE_CONTENTS_WAVY)
Lo scopo è quello di
specificare il tipo di oggetti con le quali è possibile avere delle collisioni.
Generalmente la scelta migliore è GE_CONTENTS_SOLID_CLIP che include oggetti
solidi opachi e oggetti solidi trasparenti. La costante GE_CONTENTS_EMPTY
specifica che si possono avere collisioni con oggetti empty, come ad esempio
l'acqua, se si vogliono creare effetti particolari come il
galleggiamento.
CollideFlags è una combinazione
di
#define
GE_COLLIDE_MESHES (1<<0)
#define
GE_COLLIDE_MODELS (1<<1)
#define
GE_COLLIDE_ACTORS (1<<2)
#define
GE_COLLIDE_NO_SUB_MODELS (1<<3)
#define
GE_COLLIDE_ALL (GE_COLLIDE_MESHES | GE_COLLIDE_MODELS |
GE_COLLIDE_ACTORS)
Queste costanti servono a
specificare con che cosa si può avere la collisione. GE_COLLIDE_MESHES indica
solo oggetti statici della mappa. GE_COLLIDE_MODELS include gli oggetti dinamici
della mappa (porte che si aprono). GE_COLLIDE_ACTORS indica che anche gli actor
vengono inclusi nella collisione.
UserFlag è una "maschera" che
può essere utile per limitare la collisione ad una classe di actor. Si vedrà in
seguito che in fase di creazione dell'actor si deve specificare anche lì un
valore di mask. Se con un'operazione di OR logico il valore è true allora la
collisione viene effettuata. Viene usato per definire alcuni actor con i quali
non ci deve essere collisione.
CollisionCB e Context sono due
parametri usati per richiamare una funzione di callback. Qualora i flag messi a
disposizione non siano sufficienti per definire il tipo di collisione richiesta,
allora si può fare in modo che il controllo venga eseguito da una funzione
utente. Il puntatore a tale funzione è proprio CollisionCB, mentre Context è un
puntatore agli argomenti da passare a tale funzione. Qualora la funzione
restituisca GE_TRUE allora la collisione è accettata, altrimenti viene
rifiutata.
Collision è un puntatore ad una
struttura di tipo GE_Collision
typedef
struct{
geWorld_Model *Model; // Pointer
to what model was hit (if any)
geMesh *Mesh;
// Pointer to what mesh was hit (if any)
geActor *Actor;
// Pointer to what actor was hit (if any)
geVec3d Impact;
// Impact Point
float Ratio;
// Percent from 0 to 1.0, how far along the line for the impact point
GE_Plane
Plane;
// Impact Plane
}
GE_Collision;
se viene rilevata una
collisione la struttura viene modificata con i valori corretti. Model, Mesh e
Actor conterranno un puntatore all'oggetto con il quale si ha avuto la
collisione. Impact fornisce il punto dello spazio nel quale è avvenuta la
collisione. Plane fornisce il piano con il quale è avvenuto l'impatto. Può
essere utile se si vogliono creare effetti di
scivolamento.
Dopo tutte queste spiegazioni,
capire la funzione che controlla le collisioni nel nostro programma sarà molto
semplice.
geVec3d
ControllaCollisione(geVec3d oldpos,geVec3d newpos,geBoolean *coll) {
GE_Collision
Collision;
BOOL
result;
result=geWorld_Collision(World,
&pl.Mins,
&pl.Maxs,
&oldpos,
&newpos,
GE_CONTENTS_SOLID,
GE_COLLIDE_ALL,
256,
NULL,
NULL,
&Collision); //checks for collision
if
(coll)
if (Collision.Model || Collision.Mesh)
*coll = result;
if
(result==1)
return Collision.Impact;
else
return newpos;
}
Tale funzione è una estensione
della geWorld_Collision, in quanto dati due punti nello spazio (la vecchia
posizione e la nuova posizione) la funzione restituisce due valori: un punto
(che può essere il nuovo punto se non è avvenuta la collisione o il punto di
impatto) e un booleano che indica se la collisione è avvenuta o
meno.
La funzione gravity svolge il
ruolo di far cadere il player se si trova "in aria". La funzione tenta uno
spostamento verso il basso e controlla la collisione. Se avviene abbiamo
raggiunto la posizione "a terra", altrimenti effettua lo
spostamento.
Tale spostamento è
proporzionale alla variabile speed che indica la velocità di caduta per
frame.
geBoolean
Gravity(geFloat speed) {
geFloat caduta =
-1.0f * speed;
geVec3d
normale;
geVec3d
newpos;
geBoolean
collisione = GE_FALSE;
normale =
Player_Normal(UP);
geVec3d_AddScaled(&pl.posizione, &normale, caduta,
&newpos);
pl.posizione=ControllaCollisione(pl.posizione,newpos,&collisione); // verifica la
collisione col suolo
return
collisione;
}
Nel prossimo tutorial vedremo
come aggiungere degli effetti dinamici all'ambiente, movendo Modelli e Actor
e inserendo degli effetti sonori.
Questo articolo è stato scaricato
dal Club di informatica |